package migrate

import (
	"fmt"
	"io/fs"
	"io/ioutil"
	"os"
	"path/filepath"
	"regexp"
	"strings"

	"gitee.com/githubdog/goci"
	"gitee.com/githubdog/goci/libraries/clients/xmysql"
	"gitee.com/githubdog/goci/system/db/model"
	"gitee.com/githubdog/goci/tools/helper"
)

type FileRun struct {
	FileName string
	Status   uint8 // 1 success, 2 fail, 3 executed, 4 empty
	Err      error
	Msg      string
}

type OtherConf struct {
	IgnoreStatus string
	CompareMap   map[string]string // 允许指定值, eg: @name=abc, @tag=group, @name=abc@tag=group
	ReplaceMap   map[string]string // 替换参数: ${key}, key => value
	ShowDetail   bool
}

var (
	noteReg1     = regexp.MustCompile(`(?m)^[ \t]*--.*[\n\r]?`)
	noteReg2     = regexp.MustCompile(`(?m)^[ \t]*\/\*[\s\S]*?\*\/;?[\n\r]?`)
	emptyReg     = regexp.MustCompile(`[\r\n]|\t+`)
	emptyReg2    = regexp.MustCompile(`[ ]{2,}`)
	delimiterReg = regexp.MustCompile(`(?i)^DELIMITER\s+(\S+)`)
	versionReg   = regexp.MustCompile(`\@([<>]?=?)(\d+(?:\.\d+)*)`)
	nameReg      = regexp.MustCompile(`\@(\w+)=([^\.]*)`)
)

func (oc OtherConf) formatRaplace(str string) string {
	if oc.ReplaceMap != nil {
		for k, v := range oc.ReplaceMap {
			str = strings.ReplaceAll(str, "${"+k+"}", v)
		}
	}
	return str
}

func DbMigrate(gcModel *model.Model, sqlDir, logFilePath string, oConf OtherConf) ([]FileRun, error) {
	var (
		err   error
		ret   = []FileRun{}
		_file fs.FileInfo
		files []fs.FileInfo
	)

	statusMap := map[uint8]uint{
		1: 1,
		2: 0,
		3: 3,
		4: 2,
	}
	if oConf.IgnoreStatus == "" {
		oConf.IgnoreStatus = "3"
	}

	_file, err = os.Stat(sqlDir)
	if err == nil {
		if _file.IsDir() {
			files, err = ioutil.ReadDir(sqlDir)
		} else {
			sqlDir = filepath.Dir(sqlDir)
			files = []fs.FileInfo{_file}
		}
	}
	if err == nil {
		var (
			logContent string
			item       FileRun
		)
		if logBytes, e := ioutil.ReadFile(logFilePath); e == nil {
			logContent = string(logBytes)
		}
		for k, fInfo := range files {
			item, logContent = handleMigrate(gcModel, fInfo.Name(), sqlDir, logFilePath, logContent, k, oConf)
			if item.Status > 0 {
				if !strings.Contains(","+oConf.IgnoreStatus+",", fmt.Sprintf(",%d,", item.Status)) {
					if item.Err != nil {
						item.Msg = item.Err.Error()
					}
					helper.PrintWithColor(item.FileName+": "+item.Msg, statusMap[item.Status])
					fmt.Println()
				}
				ret = append(ret, item)
			}
			if item.Err != nil {
				return ret, item.Err
			}
		}
	}

	fmt.Printf("%d files were run.\n", len(ret))
	return ret, err
}

func handleMigrate(
	gcModel *model.Model,
	filename, sqlDir, logFilePath, logContent string,
	k int, oConf OtherConf,
) (FileRun, string) {
	var dt xmysql.DbTCP
	item := FileRun{
		FileName: strings.ReplaceAll(filename, "\n", ""),
	}
	if !strings.HasSuffix(sqlDir, "/") {
		sqlDir += "/"
	}
	if strings.HasSuffix(item.FileName, ".sql") {
		if dt, item.Err = gcModel.GetWriteDb(); item.Err == nil {
			if matchs := versionReg.FindStringSubmatch(item.FileName); len(matchs) == 3 {
				wdb, _ := gcModel.GetWriteDb()
				if !helper.CompareVersion(wdb.Version, matchs[2], matchs[1]) {
					item.Status = 2
					item.Msg = fmt.Sprintf("running version must [%s %s], current version: [%s].", matchs[1], matchs[2], wdb.Version)
				}
			} else if matchs = nameReg.FindStringSubmatch(item.FileName); len(matchs) == 3 {
				if v, ok := oConf.CompareMap[matchs[1]]; !ok || v != matchs[2] {
					item.Status = 2
					item.Msg = fmt.Sprintf("%s expected '%s', got '%s'.", matchs[1], matchs[2], v)
				}
			}
			if item.Status == 0 {
				if strings.Contains(logContent, item.FileName+"\n") {
					item.Status = 3
					item.Msg = "No need to repeat"
				} else if sqlBytes, e := ioutil.ReadFile(sqlDir + item.FileName); e != nil {
					item.Err = e
				} else {
					sqlStr := string(sqlBytes)
					if arr := formatSqlString(sqlStr, dt.Conf, k); len(arr) == 0 {
						item.Status = 4
						item.Msg = "Empty file"
					} else {
						m := model.CopyModel(gcModel)
						if item.Err = m.RunTransaction(func(txm *model.Model) (txErr error) {
							for k, v := range arr {
								v = oConf.formatRaplace(v)
								if oConf.ShowDetail {
									outStr := v
									runeVal := []rune(outStr)
									if len(runeVal) > 200 {
										outStr = string(runeVal[:180]) + " ...... " + string(runeVal[len(runeVal)-10:])
									}
									startSymbol := "─"
									if len(arr) > 1 {
										if k == 0 {
											startSymbol = "┌─"
										} else if k+1 == len(arr) {
											startSymbol = "└─"
										} else {
											startSymbol = "├─"
										}
									}
									outStr = "  " + startSymbol + " " + outStr
									fmt.Println(outStr)
								}
								txm.SetQueryRaw(v)
								if _, txErr = txm.ExecQuery(); txErr != nil {
									break
								}
							}
							if txErr == nil && logFilePath != "" {
								logContent += item.FileName + "\n"
								txErr = ioutil.WriteFile(logFilePath, []byte(logContent), goci.NormalFilePerm)
							}
							return
						}); item.Err == nil {
							item.Status = 1
							item.Msg = "success"
						} else {
							item.Status = 2
						}
					}
				}
			}
		}
	}
	return item, logContent
}

func GetSqlStringByFile(gcModel *model.Model, fileName string) ([]string, error) {
	var (
		err      error
		ret      []string
		sqlBytes []byte
	)

	if sqlBytes, err = ioutil.ReadFile(fileName); err == nil {
		sqlStr := string(sqlBytes)
		ret = formatSqlString(sqlStr, gcModel.GetOriginWriteDb().TCP.Conf, 0)
	}

	return ret, err
}

func formatSqlString(str string, conf xmysql.DBConf, k int) []string {
	var (
		ret          []string
		delimiterStr = ";"
		rStr         = "><(@J67J0ooodsk__++@!$!$,,=,,,,$(-|---$44￥￥*4jfds&><"
		replaceN     int
	)

	str = noteReg1.ReplaceAllString(str, "")
	str = noteReg2.ReplaceAllString(str, "")
	for forIndex := 0; str != ""; forIndex++ {
		replaceMap := map[string]string{}
		quote1Reg := regexp.MustCompile(`[^\\]'([^'\n]|\\')*(DELIMITER|` + regexp.QuoteMeta(delimiterStr) + `)([^'\n]|\\')*[^\\]'`)
		quote2Reg := regexp.MustCompile(`[^\\]"([^"\n]|\\")*(DELIMITER|` + regexp.QuoteMeta(delimiterStr) + `)([^"\n]|\\")*[^\\]"`)
		replaceFunc := func(s string) string {
			s0 := strings.ReplaceAll(fmt.Sprintf("%s-_=#%d-_=#%dEND", rStr, k, replaceN), delimiterStr, "")
			replaceN++
			replaceMap[s0] = s
			return s0
		}
		str = quote1Reg.ReplaceAllStringFunc(str, replaceFunc)
		str = quote2Reg.ReplaceAllStringFunc(str, replaceFunc)
		if forIndex == 0 {
			str = emptyReg.ReplaceAllString(str, " ")
			str = emptyReg2.ReplaceAllString(str, " ")
		}
		index := strings.Index(strings.ToUpper(str), "DELIMITER")
		if index == -1 {
			ret = append(ret, splitByDelimiter(str, delimiterStr, replaceMap, conf)...)
			break
		} else {
			if index > 0 {
				ret = append(ret, splitByDelimiter(str[:index], delimiterStr, replaceMap, conf)...)
			}
			str = str[index:]
			if matchs := delimiterReg.FindStringSubmatch(str); len(matchs) >= 2 {
				str = str[len(matchs[0]):]
				delimiterStr = matchs[1]
			} else {
				break
			}
		}
	}

	return ret
}

func splitByDelimiter(str, d string, replaceMap map[string]string, conf xmysql.DBConf) []string {
	var ret []string
	for _, v := range strings.Split(str, d) {
		if v = helper.Trim(v); v != "" {
			for k2, v2 := range replaceMap {
				v = strings.ReplaceAll(v, k2, v2)
			}
			if strings.Contains(v, "{{{$DATABASE}}}") {
				var leftSide, rightSide string
				if conf.DBname == "" {
					leftSide = "`?"
					rightSide = "`?\\."
				}
				dbNameReg := regexp.MustCompile(leftSide + `\{\{\{\$DATABASE\}\}\}` + rightSide)
				v = dbNameReg.ReplaceAllString(v, conf.DBname)
			}
			v = strings.ReplaceAll(v, "{{{$DBPREFIX}}}", conf.Prefix)
			v = strings.ReplaceAll(v, "{{{$TABLE}}}", conf.Prefix)
			v = strings.ReplaceAll(v, "{{{$CHARSET}}}", conf.Other["charset"])
			v = strings.ReplaceAll(v, "{{{$COLLATION}}}", conf.Other["collation"])
			ret = append(ret, v)
		}
	}
	return ret
}
